1. React.memo

当父组件被重新渲染的时候,也会触发子组件的重新渲染,这样就多出了无意义的性能开销。如果子组件的状态没有发生变化,则子组件是不需要被重新渲染的。

我们可以使用 React.memo 来解决上述的问题,从而达到提高性能的目的。

注意:React.memo不是hooks,而是react的api。

image-20231128114600357

React.memo 的语法格式如下:

例如,在下面的代码中,父组件声明了 count 和 flag 两个状态,子组件依赖于父组件通过 props 传递的 num。当父组件修改 flag 的值时,会导致子组件的重新渲染:

我们使用 React.memo(函数式组件) 将子组件包裹起来,只有子组件依赖的 props 发生变化的时候,才会触发子组件的重新渲染。示例代码如下:

也可以直接从react导入类型和方法,简写成这样:

 

2. useMemo

1. 问题引入

进一步改造前面的案例:我们希望在 Father 组件中添加一个“计算属性”,根据 flag 值的真假,动态返回一段文本内容,并把计算的结果显示到页面上。示例代码如下:

代码编写完毕后,我们点击父组件中的 +1 按钮,发现 count 在自增,而 flag 的值不会发生变化。此时也会触发 tips 函数的重新执行,这就造成了性能的浪费。我们希望如果 flag 没有发生变化,则避免 tips 函数的重新计算,从而优化性能。此时需要用到 React Hooks 提供的 useMemo API。

2. useMemo 的语法格式

useMemo 的语法格式如下:

其中:

  1. cb:这是一个函数,用于处理计算的逻辑,必须使用 return 返回计算的结果

  2. array:这个数组中存储的是依赖项,只要依赖项发生变化,都会触发 cb 的重新执行。使用 array 需要注意以下几点

    • 不传数组,则每次更新都会重新计算
    • 空数组,则只会计算一次
    • 依赖对应的值,则对应的值发生变化时会重新执行 cb

3. 使用 useMemo 解决刚才的问题

  1. 导入 useMemo:

  2. 在 Father 组件中,使用 useMemo 对 tips 进行改造:

  3. 此时,点击 Father 中的 +1 按钮,并不会触发 tips 的重新计算,而是会使用上一次缓存的值进行渲染。只有依赖项 flag 变化时,才会触发 tips 的重新计算。

3. useCallback

语法格式

之前我们所学的 useMemo 能够达到缓存某个变量值的效果,而当前要学习的 useCallback 用来对组件内的函数进行缓存,它返回的是缓存的函数。它的语法格式如下:

useCallback 会返回一个 memorized 回调函数供组件使用,从而防止组件每次 rerender 时反复创建相同的函数,能够节省内存开销,提高性能。其中:

  1. cb 是一个函数,用于处理业务逻辑,这个 cb 就是需要被缓存的函数

  2. array 是依赖项列表,当 array 中的依赖项变化时才会重新执行 useCallback。

    • 如果省略 array,则每次更新都会重新计算
    • 如果 array 为空数组,则只会在组件第一次初始化的时候计算一次
    • 如果 array 不为空数组,则只有当依赖项的值变化时,才会重新计算

基本示例

接下来,我们通过下面的例子演示使用 useCallback 的必要性:当输入框触发 onChange 事件时,会给 kw 重新赋值。kw 值的改变会导致组件的 rerender,而组件的 rerender 会导致反复创建 onKwChange 函数并添加到 Set 集合中,造成了不必要的内存浪费。代码如下:

运行上面的代码,我们发现每次文本框的值发生变化,都会打印 set.size 的值,而且这个值一直在自增 +1,因为每次组件 rerender 都会创建一个新的 onKwChange 函数添加到 set 集合中。

为了防止 Search 组件 rerender 时每次都会重新创建 onKwChange 函数,我们可以使用 useCallback 对这个函数进行缓存。改造后的代码如下:

运行改造后的代码,我们发现无论 input 的值如何发生变化,每次打印的 set.size 的值都是 1。证明我们使用 useCallback 实现了对函数的缓存。

另外一个对于input的onChange绑定事件的优化,就是直接在onChange属性里面写函数,这样应该不会生成多个函数,可以试一试。

useCallback 的案例

1. 问题引入

  1. 导入需要的 hooks 函数,并定义需要的 TS 类型:

  2. 定义 SearchInput 搜索框子组件,接收父组件传递进来的 onChange 处理函数,每当 input 触发 onChange 事件时,调用 props.onChange 进行处理:

  3. 定义 SearchResult 搜索结果子组件,接收父组件传递进来的 query 搜索关键字,在 useEffect 中监听 props.query 的变化,从而请求搜索的结果:

  4. 定义父组件 SearchBox 并渲染 SearchInput 组件和 SearchResult 组件。在父组件中监听 SearchInput 的 onChange 事件,并把父组件中定义的处理函数 onKwChange 传递进去。同时,把父组件中定义的搜索关键字 kw 传递给 SearchResult 组件。示例代码如下:

  5. 经过测试后,我们发现:

    SearchResult子组件是需要跟随kw的变化重新渲染的,这个不用管。这里重点关注的是SearchInput子组件的更新过程。

    1. 每当SearchInput子组件的文本框内容发生变化,都会调用 props.onChange 把数据发送给父组件。
    2. 相应的,父组件通过 onKwChange 函数可以获取到SearchInput子组件的值,并把值更新到 kw 中。当 kw 发生变化,会触发父组件的 rerender。
  6. 而父组件的 rerender 又会重新生成 onKwChange 函数并把函数的引用作为 props 传递给SearchInput子组件。

    1. 这样,SearchInput子组件就监听到了 props 的变化,最终导致SearchInput子组件的 rerender。

    其实,SearchInput子组件根本不需要被重新渲染,因为 props.onChange 函数的处理逻辑没有发生变化,只是它的引用每次都在变。为了解决这个问题,我们需要用到 useCallback 和 React.memo。

2. 问题解决

  1. 首先,我们需要让子组件 SearchInput 被缓存,所以我们需要使用 React.memo 对其进行改造:

  2. 使用 React.memo 对组件进行缓存后,如果子组件的 props 在两次更新前后没有任何变化,则被 memo 的组件不会 rerender。

    所以为了实现 SearchInput 的缓存,还需要基于 useCallback 把父组件传递进来的 onChange 进行缓存。

    在父组件中针对 onKwChange 调用 useCallback,示例代码如下:

  3. 经过测试,我们发现每当文本框内容发生变化,不会导致 SearchInput 组件的 rerender。

思路要理清楚,首先是onKwChange函数只需要生成一个,所以用到了useCallback方法,然后是SearchInput组件里面,父组件传递过来的属性onChange并没有发生变化,所以不需要重新渲染,于是用到了React.memo。

这套组合拳打的很经典,要灵活运用。

4. useTransition

1. 问题引入

useTransition 可以将一个更新转为低优先级更新,使其可以被打断,不阻塞 UI 对用户操作的响应,能够提高用户的使用体验。它常用于优化视图切换时的用户体验。

例如有以下3个标签页组件,分别是 Home、Movie、About,其中 Movie 是一个渲染特别耗时的组件,在渲染 Movie 组件期间页面的 UI 会被阻塞,用户会感觉页面十分卡顿,示例代码如下:

配套的 CSS 样式为:

当movieTab里面是简单的字符串时,可以很轻松的切换:

当movieTab里面是大数据时,可以看到不光UI没有渲染出来,连button切换也不能进行了:

2. 语法格式

因为在默认情况下,react会先执行渲染操作而不是响应用户的操作,所以当切换到Movie组件的时候,react会先渲染Movie组件,但是Movie组件渲染非常耗时,所以这时候用户想切换到其它组件,react也是不会响应的。这时候就要用到useTransition这个hook。

参数:

返回值(数组):

3. 问题解决

修改 TabsContainer 组件,使用 useTransition 把点击按钮后为 activeTab 赋值的操作,标记为低优先级。此时 React 会优先响应用户对界面的其它操作,从而保证 UI 不被阻塞:

此时,点击 Movie 按钮后,状态的更新被标记为低优先级,About 按钮的 hover 效果和点击操作都会被立即响应。

4. 使用 isPending 展示加载状态

为了能够使用 isPending 的状态为按钮添加 loading 效果,我们需要把 useTransition 的调用从 TabsContainer 组件中挪到 TabButton 组件中:

用老师提供的代码,可以看到isPending起作用了,按钮的内容发生了变化:

但是我的代码中,isPending没有起作用,界面没有按照预想发生变化,为什么呢?

在Movie组件中,将父组件中传递过来的isPending输出,结果是false。说明刚开始渲染的时候,useTransition并没有将isPending设置为true,只有等到遇到后面的代码之后,才判断出要将isPending设置为true,而这个时候,Movie组件处在渲染过程中,那么isPending判断的那段代码是永远不会执行的。

可以看到,在点击movie按钮时,传递过来的isPending是false,切换到别的tab时,isPending的值是true,说明只有遇到费时的渲染代码时,react才会将isPending设置为true。

为什么老师代码里面可以使用isPending呢?因为useTransition定义在button组件中,在点击button时,绑定了startTransition事件,这样会触发父组件的setActiveTab,会造成父组件的重新渲染,里面的子组件也会重新渲染,此时定义在button里面的isPending就被设置为true了,所以可以正常使用。

那么我这样说看可不可以:isPending只能用在当前组件中,如果通过props传递,会有状态的延迟而达不到效果。

 

那如果就是想在Movie组件里面设置loading界面,应该怎么做呢?我想的话,应该不能将isPending状态定义在父组件中,这样传递给子组件的值就没有时间进行更改,应该定义在子组件中,然后传递给父组件,再通过父组件传递给另外的需要用到isPending的子组件。这个方法行不通。为什么呢?还是isPending值不是最新的问题,只有再次进入到button组件中,isPending才是最新值,此时该怎么执行组件的事件呢?我刚才是在onClick的时候传值的,现在呢?该怎么传值?

上面这种写法更加复杂,从button中获取pending状态,再传递到Movie组件中,是最新值吗?

 

我看视频里面老师是这么做的,在TabsContainer组件里面定义专门的渲染组件的函数:

效果非常好:

仔细看老师的代码,和我的代码有什么不同?我在上面有一段代码{active === "movie" ? isPending ? "数据请求中,请稍后" : <Movie /> : ""},老师是先判断isPending的,然后再渲染具体的组件,我是先判断应该渲染哪个组件,然后判断isPending,先将我的代码按照老师的顺序测试一下,看效果。

为什么我的这种判断方式不行呢?尝试输出一下isPending和active的值,看一下到底是怎么变化的:

image-20240219142738936

可以看到isPending为true时,active的状态是原来的状态,isPending为false了,active才会变为最新值。那么按照我的判断条件:{active === "movie" ? isPending ? "数据请求中,请稍后" : <Movie /> : ""},active变为movie了,isPending就为false了,loading状态只有在isPending从false变为true再变为false时才会显示,从上面可以看出,这个时间非常短,根本达不到loading的需求。

而renderTabs()方法里面,当isPending变为true之后,页面渲染的就是loading...,后来isPending变为了false,就会渲染耗时的组件,而直到这个组件真正渲染到页面上之前,原来的界面loading...是不会注销的,就相当于间接达到了目的。

这整个流程一定要搞清楚,我根本没有想到耗时组件渲染之前的情况是什么样的,考虑到这一点就好理解了。

 

那么先判断isPending,再渲染组件,写成这样行不行呢?

这样是用到了pending,但是在home切换到movie的时候,会有三个“数据请求中,请稍后”,因为此时三个表达式都满足条件,所以都会被渲染出来。还是要按照老师的,写一个函数出来,首先就判断isPending,然后再分别渲染组件。

image-20240219141107838

5. 注意事项

  1. 传递给 startTransition 的函数必须是同步的。React 会立即执行此函数,并将在其执行期间发生的所有状态更新标记为 transition。如果在其执行期间,尝试稍后执行状态更新(例如在一个定时器中执行状态更新),这些状态更新不会被标记为 transition。

 

  1. 标记为 transition 的状态更新将被其他状态更新打断。例如在 transition 中更新图表组件,并在图表组件仍在重新渲染时继续在输入框中输入,React 将首先处理输入框的更新,之后再重新启动对图表组件的渲染工作。
  2. transition 更新不能用于控制文本输入。

5. useDeferredValue

1. 问题引入

在搜索框案例中,SearchResult 组件会根据用户输入的关键字,循环生成大量的 p 标签,因此它是一个渲染比较耗时的组件。代码如下:

可以看到,刷新浏览器之后我就输入了,很久之后才有反应。渲染还是非常耗时的,组件对输入的每一种情况都给出了响应。但很可能用户需要的并不是每一种情况都给出响应,而是输出一段话之后,根据这段话响应就行了。

注意,此案例不能使用 useTransition 进行性能优化,因为 useTransition 会把状态更新标记为低优先级,被标记为 transition 的状态更新将被其他状态更新打断。因此在高频率输入时,会导致中间的输入状态丢失的问题。比如说输入了123,在输入1会触发setKw(e.currentTarget.value)这个方法,但是这个状态更新是被startTransition包裹了,所以会异步执行,那么当用户很快输入2时,react会优先响应用户操作,这个状态更新会被UI更新打断,然后如果用户很快输入3,前面的更新也会被打断,最后输入框里面就可能只剩下3来显示了。

例如:

我快速输入了123456,结果界面值渲染了6。

这里有一个现象,就是input组件直接写在SearchBox里面的时候,是会产生输入丢失的现象,但是如果将输入框作为一个单独的组件,使用useTransition,并没有输入丢失的现象:

这是为什么呢?暂时不知道原因。

2. 语法格式

useDeferredValue 提供一个 state 的延迟版本,根据其返回的延迟的 state 能够推迟更新 UI 中的某一部分,从而达到性能优化的目的。语法格式如下:

useDeferredValue 的返回值为一个延迟版的状态:

  1. 在组件首次渲染期间,返回值将与传入的值相同
  2. 在组件更新期间,React 将首先使用旧值重新渲染 UI 结构,这能够跳过某些复杂组件的 rerender,从而提高渲染效率。随后,React 将使用新值更新 deferredValue,并在后台使用新值重新渲染是一个低优先级的更新。这也意味着,如果在后台使用新值更新时 value 再次改变,它将打断那次更新。

3. 问题解决

按需导入 useDeferredValue 这个 hooks API,并基于它进行搜索功能的性能优化:

虽然渲染还是耗时,但是对用户的响应做到了只对最后的输入做出响应,这也是一种优化。

4. 表明内容已过时

当 kw 的值频繁更新时,deferredKw 的值会明显滞后,此时用户在页面上看到的列表数据并不是最新的,为了防止用户感到困惑,我们可以给内容添加 opacity 透明度,表明当前看到的内容已过时。示例代码如下:

效果还是蛮好的。

对这个教程中涉及到Typescript的地方,总结一下写法:

 

image-20231214171201906

props的类型有时候这样写,有时候写成React.FC<React.PropsWithChildren>,要分清楚情况,后面这种情况是因为在子组件里面使用了{props.children}这样的代码,好好总结一下。

-----2024-02-19解答:

这是泛型的使用,不要和泛型的定义搞混了,在React.FC这个类型中,定义了一个泛型参数P,而这个P是使用者自己来定义的类型,在使用泛型的时候,需要传递一个类型参数,无论是上面图中的React.FC<{isPending:boolean}>,还是React.FC<React.PropsWithChildren>,还是下面更复杂的:

其实都是在自定义泛型P,都是用在props中的。

泛型的定义和使用,有点眼花。

像上面定义input的onChange事件函数的时候,就是泛型的使用,而不是定义:

还有定义组件类型的时候,也是泛型的使用:

image-20240219092613649

那么定义泛型的时候,有什么不同的地方呢?现在我还说不上来,就现在心里把泛型的定义和使用分开来看待就行了。

&&符号的使用?

&&我只知道是一个逻辑判断运算符,表示“逻辑与”。但是用在react中是什么意思呢?经常看到这种用法:{isPending && "..."},这是什么意思?

这不是react专门的用法,而是JS的用法。

在 JavaScript 中,true && expression 总是会返回 expression, 而 false && expression 总是会返回 false。我之前的用法只是两个boolean值来进行计算,比如if(isActive && data.length > 0){},所以没有想到会有这种效果。

image-20231215153717635

在react中,&&右边的expression可以是字符串,也可以是一个组件。

对象数组怎么定义类型

在学习typescript的时候,定义的数组都是基本类型,比如说:(number | srting)[]或者Array<number | string>,那复杂一点的对象数组该怎么定义类型呢?其实就是为数组加上对象类型就行了,比如说:

image-20240219105738299

useState里面的数据类型怎么定义?

在使用useState定义数据的时候,基本类型的数据还好说,直接定义,ts会自动为我们推导出类型,但是复杂一点的数据,比如说对象数组、对象等等,不给出类型直接定义,在使用的时候就会有红色波浪线的提示:

image-20240219111128145

定义的list是一个对象数组,里面有一个属性word,在渲染的时候会用到,由于没有定义list的类型,所以会报红色波浪线。

那么该怎么定义数据类型呢?

image-20240219110524312

从react官方的TS定义来看,useState需要传递一个泛型参数进去即可。

那么可以写成这样:

image-20240219111343155

这样就OK了。

使用函数返回值时怎么写ts

在使用函数返回值时,由于惯性的思维,想直接为这个变量定义类型,就像这样自然:

但是函数返回值有一点很不同的地方,就是ts里面的函数基本上已经定义好了参数类型和返回值类型,而且如果函数里面使用了泛型的话,那么调用函数的时候,只需要指定泛型类型即可,函数的参数和返回值都会用到泛型的类型,这样用起来就非常简洁了。

我这里记录一下我的一种操作,在定义zustand的store的时候,useStore是create函数的返回值,于是我经常在useStore的后面定义类型,可想而知这是不行的、错误的,我根本就不知道useStore的类型是什么,而不需要知道,只需要在create时候,传递泛型参数进去即可,create函数已经定义好了参数和返回值的类型,ts会帮助我们定义出useStore的类型,这里就不需要定义类型了。

如果有什么顾虑,可以直接看阮一峰的教程,函数一章。

在编写single-store的时候,是这样定义类型的,对于我这个typescript初学者来说,确实容易记混:

为什么这里是这样做呢?原因是这个返回值本质上是一个函数,实际上我们定义的是函数的类型,查看一下StateCreator的typescript原始定义:

可以看到类型定义主体是这个:

就是一个函数的类型定义。

实际上是没有看类型的原始定义、没有学透typescript造成的混乱。